use std::{cell::RefCell, rc::Rc};

use crate::{
    bitmap::Bitmap, 
    block_cache_dev::BlockCacheDevice, 
    block_dev::BlockDevice, 
    layout::{
        super_block::SuperBlock, DataBlock, DiskInode, DiskInodeKind, DirectoryEntry, DIRENT_SIZE,
    }, vfs::Inode, BlockID, BLOCK_SIZE
};

#[derive(Clone, Copy)]
pub struct DiskInodePosition {
    pub block_id: BlockID, 
    pub offset: usize,
}

pub struct FileSystem {
    pub device: Box<BlockCacheDevice>,
    pub inode_bitmap: Bitmap,
    pub data_bitmap: Bitmap,
    pub inode_area_start_block: BlockID,
    pub data_area_start_block: BlockID,
}

impl FileSystem {
    pub fn create(
        total_block_size: u32,
        inode_bitmap_block_size: u32,
        device: Box<dyn BlockDevice>) 
    {
        let inode_bitmap = Bitmap::new(1, inode_bitmap_block_size as usize);
        let inode_size = inode_bitmap.manage_size();
        let inode_area_block_size = 
            ((inode_size * core::mem::size_of::<DiskInode>() + BLOCK_SIZE - 1) / BLOCK_SIZE) as u32;
        let inode_total_block_size = inode_bitmap_block_size + inode_area_block_size;

        let data_total_block_size = total_block_size - inode_total_block_size - 1;
        let data_bitmap_block_size = (total_block_size + 4096) / 4097;
        let data_area_block_size = data_total_block_size - data_bitmap_block_size;
        let data_bitmap = Bitmap::new(1 + inode_bitmap_block_size, data_bitmap_block_size as usize);

        let mut device = BlockCacheDevice::new(device);
        // Clear device
        for block_id in 0..total_block_size {
            let data: &mut DataBlock = device.get_mut(block_id, 0);
            for byte in data.as_mut_slice() {
                *byte = 0;
            }
        }
        
        // Write SuperBlock
        let super_block: &mut SuperBlock = device.get_mut(0, 0);
        super_block.initialize(
            total_block_size, 
            inode_bitmap_block_size, 
            inode_area_block_size, 
            data_bitmap_block_size, 
            data_area_block_size
        );

        // Create root
        // SuperBlock | Inode Bitmap | Data Bitmap | Inode Area | Data Area |
        let mut fs = Self {
            device: Box::new(device),
            inode_bitmap,
            data_bitmap,
            inode_area_start_block: 1 + inode_bitmap_block_size + data_bitmap_block_size,
            data_area_start_block: 1 + inode_bitmap_block_size + data_bitmap_block_size + inode_area_block_size, 
        };

        let root_inode_id = fs.alloc_inode_id();
        assert!(root_inode_id == 0);

        let mut root_inode = DiskInode::new(DiskInodeKind::Directory);
        root_inode.increase_size((2 * DIRENT_SIZE) as u32, &mut fs);

        // Add "." and ".."
        // "."
        let self_entry = DirectoryEntry::new(".", root_inode_id as u32);
        root_inode.write(0, &self_entry, &mut fs);

        // ".."
        let parent_entry = DirectoryEntry::new("..", root_inode_id as u32);
        root_inode.write(DIRENT_SIZE, &parent_entry, &mut fs);

        // Save root_inode
        let root_inode_pos = fs.disk_inode_id_to_position(root_inode_id);
        *fs.device.get_mut(root_inode_pos.block_id, root_inode_pos.offset) = root_inode;
    }

    pub fn open(device: Box<dyn BlockDevice>) -> Rc<RefCell<Self>> {
        let mut device = BlockCacheDevice::new(device);
        let sb: SuperBlock = *device.get_ref(0, 0);

        // SuperBlock | Inode Bitmap | Data Bitmap | Inode Area | Data Area |
        Rc::new(RefCell::new(Self {
            device: Box::new(device),
            inode_bitmap: Bitmap::new(1, sb.inode_bitmap_block_size as usize),
            data_bitmap: Bitmap::new(1 + sb.inode_bitmap_block_size, sb.data_bitmap_block_size as usize),
            inode_area_start_block: 1 + sb.inode_bitmap_block_size + sb.data_bitmap_block_size,
            data_area_start_block: 1 + sb.inode_bitmap_block_size + sb.data_bitmap_block_size + sb.inode_area_block_size,
        }))
    }
}

impl FileSystem {
    pub fn root_inode(fs: &Rc<RefCell<Self>>) -> Inode {
        let root_pos = fs.borrow().disk_inode_id_to_position(0);
        Inode::new(root_pos, fs.clone())
    }
}

const DISK_INODE_BYTE_SIZE: usize = std::mem::size_of::<DiskInode>();
const DISK_INODE_SIZE_A_BLOCK: usize = BLOCK_SIZE / DISK_INODE_BYTE_SIZE;

impl FileSystem {
    pub fn alloc_data_block_id(&mut self) -> BlockID {
        self.data_bitmap.alloc(&mut self.device).unwrap() as BlockID + self.data_area_start_block
    }

    pub fn alloc_inode_id(&mut self) -> usize {
        self.inode_bitmap.alloc(&mut self.device).unwrap()
    }

    pub fn dealloc_data_block_id(&mut self, block_id: BlockID) {
        let block_index = (block_id - self.data_area_start_block) as usize;
        self.data_bitmap.dealloc(block_index, &mut self.device);
    }

    pub fn dealloc_inode_id(&mut self, inode_id: usize) {
        self.inode_bitmap.dealloc(inode_id, &mut self.device);
    }

    pub fn disk_inode_id_to_position(&self, inode_id: usize) -> DiskInodePosition {
        let block_id = self.inode_area_start_block + (inode_id / DISK_INODE_SIZE_A_BLOCK) as u32;
        let offset = (inode_id % DISK_INODE_SIZE_A_BLOCK) * DISK_INODE_BYTE_SIZE;
        DiskInodePosition {
            block_id,
            offset
        }
    }

    pub fn disk_position_to_inode_id(&self, inode_pos: &DiskInodePosition) -> usize {
        (inode_pos.block_id - self.inode_area_start_block) as usize * DISK_INODE_SIZE_A_BLOCK + 
        (inode_pos.offset / DISK_INODE_BYTE_SIZE)
    }
}